<?php

namespace Cephalopod\SSL;

use Exception;

class OpenSSL
{
    const KEY_BITS = 512;

    public static function generatePrivateKeyAndPublicKey(string $directory): bool
    {
        if (!is_dir($directory)) {
            return false;
        }
        $localKeyFolder = $directory . DIRECTORY_SEPARATOR . 'local';
        $remoteKeyFolder = $directory . DIRECTORY_SEPARATOR . 'remote';
        if ((!is_dir($localKeyFolder) && !mkdir($localKeyFolder)) || (!is_dir($remoteKeyFolder) && !mkdir($remoteKeyFolder))) {
            return false;
        }
        $localPrivateKey = $localKeyFolder . DIRECTORY_SEPARATOR . 'local_private.pem';
        $localPublicKey = $remoteKeyFolder . DIRECTORY_SEPARATOR . 'local_public.pem';
        $remotePrivateKey = $remoteKeyFolder . DIRECTORY_SEPARATOR . 'remote_private.pem';
        $remotePublicKey = $localKeyFolder . DIRECTORY_SEPARATOR . 'remote_public.pem';
        if (!static::generateKeyFiles($localPrivateKey, $localPublicKey) || !static::generateKeyFiles($remotePrivateKey, $remotePublicKey)) {
            return false;
        }
        return true;
    }

    public static function encrypt(string $originData, string $publicKey): string
    {
        $originDataSize = strlen($originData);
        // bits => byte. If PKCS#1 (OPENSSL_PKCS1_PADDING) is used, the allowed length of raw data must be 11 bytes less.
        $maxDataBlockSize = self::KEY_BITS / 8 - 11;
        if ($originDataSize <= $maxDataBlockSize) {
            return static::encryptDataBlock($originData, $publicKey);
        }
        $encryptedDataSize = 0; // The length of the encrypted data in the original data
        $encryptedData = '';
        do {
            $needEncryptDataSize = $originDataSize - $encryptedDataSize;
            // The current encrypted data size
            $encryptDataSize = $needEncryptDataSize > $maxDataBlockSize ? $maxDataBlockSize : $needEncryptDataSize;
            $encryptedBlockData = static::encryptDataBlock(substr($originData, $encryptedDataSize, $encryptDataSize), $publicKey);
            $encryptedData .= $encryptedBlockData;
            $encryptedDataSize += $encryptDataSize;
        } while ($encryptedDataSize < $originDataSize);
        return $encryptedData;
    }

    public static function decrypt(string $encryptedData, string $privateKey): string
    {
        $encryptedDataSize = strlen($encryptedData);
        $dataBlockSize = self::KEY_BITS / 8;
        // encryptedDataSize must be an integral multiple of dataBlockSize
        if (0 != ($encryptedDataSize % $dataBlockSize) || empty($encryptedData)) {
            throw new Exception('Data size error');
        }
        $decryptedDataSize = 0;
        $decryptedData = '';
        do {
            $decryptedDataBlock = static::decryptDataBlock(substr($encryptedData, $decryptedDataSize, $dataBlockSize), $privateKey);
            $decryptedDataSize += $dataBlockSize;
            $decryptedData .= $decryptedDataBlock;
        } while($decryptedDataSize < $encryptedDataSize);
        return $decryptedData;
    }

    private static function generateKeyFiles(string $privateKeyFile, string $publicKeyFile): bool
    {

        if (false === ($privateKeyInfo = openssl_pkey_new([
            'private_key_bits' => self::KEY_BITS,
            'private_key_type' => OPENSSL_KEYTYPE_RSA,
        ]))) {
            return false;
        }
        if (!openssl_pkey_export_to_file($privateKeyInfo, $privateKeyFile)) {
            return false;
        }
        if (false === ($info = openssl_pkey_get_details($privateKeyInfo))) {
            return false;
        }
        if (!file_put_contents($publicKeyFile, $info['key'])) {
            return false;
        }
        return true;
    }

    private static function encryptDataBlock(string $originData, string $publicKey): string
    {
        $encryptedData = '';
        if (!openssl_public_encrypt($originData, $encryptedData, $publicKey, OPENSSL_PKCS1_PADDING)) {
            throw new Exception('Encryption failed');
        }
        return $encryptedData;
    }

    private static function decryptDataBlock(string $originData, string $privateKey): string
    {
        $decryptedData = '';
        if (!openssl_private_decrypt($originData, $decryptedData, $privateKey, OPENSSL_PKCS1_PADDING)) {
            throw new Exception('Decryption failed');
        }
        return $decryptedData;
    }
}
